package Dungeon;

import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import be.kuleuven.cs.som.annotate.Model;

import SquarePackage.Direction;
import SquarePackage.Square;

/**
 * Class for a composed dungeon. This is a dungeon with subdungeons in it.
 * 
 * @invar	All coordinates are positive and smaller than the maximum value.
 * 			| isValidDimension()
 * 
 * @author 	Brecht Reynders en Florian Hendrickx
 * @version 1.0
 */
public class ComposedDungeon extends Subdungeon {
	
	/**
	 * Initializes a new dungeon with the given coordinates.
	 * 
	 * @param	maxX
	 * 			The maximum x-coordinate value.
	 * @param	maxY
	 * 			The maximum y-coordinate value.
	 * @param	maxZ
	 * 			The maximum z-coordinate value.
	 * @effect	The given coordinates are the coordinates of the maximum dimension.
	 * 			| super(maxX, maxY, maxZ) 
	 */
	public ComposedDungeon(long maxX, long maxY, long maxZ){
		super(maxX, maxY, maxZ);
		this.dungeons = new HashMap<Dimension, Subdungeon>();
	}
		
	/**
	 * Initializes a new dungeon with the given dimension as maximum dimension.
	 * 
	 * @param 	maxDimension
	 * 			The maximum dimension
	 * @effect	The given maximum dimension is the new max dimension.
	 * 			| super(maxDimension)
	 */
	public ComposedDungeon(Dimension maxDimension){
		super(maxDimension);
		this.dungeons = new HashMap<Dimension, Subdungeon>();
	}

	/**
	 * Variable registering the dungeons in this composed dungeon and their starting position.
	 */
	private Map<Dimension, Subdungeon> dungeons;
	
	/**
	 * Returns the dungeons and their starting dimensions.
	 */
	public Map<Dimension,Subdungeon> getDungeons(){
		return dungeons;
	}
	
	/**
	 * Returns all the starting dimensions of the dimensions of this composed dungeon.
	 */
	public Set<Dimension> getDimensions(){
		return dungeons.keySet();
	}
	
	/**
	 * Returns all the dungeons in this composed dungeon.
	 */
	public Collection<Subdungeon> getAllDungeons(){
		return getDungeons().values();
	}
	
	/**
	 * Returns all the squares in this dungeon.
	 */
	public Collection<Square> getAllSquares(){
		List<Square> allSquares = new ArrayList<Square>();
		for(Subdungeon dungeon: getAllDungeons()){
			allSquares.addAll(dungeon.getAllSquares());
		}
		return allSquares;
	}
	
	/**
	 * Adds a subdungeon to this dungeon.
	 * 
	 * @param 	subdungeon
	 * 			The dungeon to be added to this dungeon.
	 * @post	
	 */
	public void addDungeon(Subdungeon subdungeon, Dimension dimension)throws IllegalArgumentException{
		if(subdungeon.hasContainingDungeon())
			throw new IllegalArgumentException("The given subdungeon already is in a composed dungeon.");
		if(!fit(dimension, subdungeon))
			throw new IllegalArgumentException(dimension + " is not a valid place to add dungeon " + subdungeon);
		dungeons.put(dimension, subdungeon);
		subdungeon.setContainingDungeon(this);
		mergeDungeon(subdungeon);
		//TODO merge rooms
	}
	
	/**
	 * 
	 * @param subdungeon
	 */
	public void mergeDungeon(Subdungeon subdungeon){
		for(Dimension dimension: subdungeon.getUsedDimensions()){
			if(dimension.getXPosition()==0){
				mergeSquares(dimension,Direction.WEST,subdungeon);
			}
			if(dimension.getXPosition()==subdungeon.getMaxDimension().getXPosition()){
				mergeSquares(dimension,Direction.EAST,subdungeon);
			}
			if(dimension.getYPosition()==0){
				mergeSquares(dimension,Direction.NORTH,subdungeon);
			}
			if(dimension.getYPosition()==subdungeon.getMaxDimension().getYPosition()){
				mergeSquares(dimension,Direction.SOUTH,subdungeon);
			}
			if(dimension.getZPosition()==0){
				mergeSquares(dimension,Direction.FLOOR,subdungeon);
			}
			if(dimension.getZPosition()==subdungeon.getMaxDimension().getZPosition()){
				mergeSquares(dimension,Direction.CEILING,subdungeon);
			}
		}
	}
	
	private void mergeSquares(Dimension dimension, Direction direction, Subdungeon subdungeon){
		Square square = subdungeon.getSquareAt(dimension);
		Square other = getRootDungeon().getSquareAt(getRootDimension(dimension,this).getDimensionIn(direction));
		if(other!=null){
			if(other!=null){
				if(!square.getBorderAt(direction).getIsolation()&&!other.getBorderAt(direction.getOpposite()).getIsolation()){
					getRoomOf(other).mergeWith(getRoomOf(square));
				}
				setBorder(square, other, direction);
			}
		}
	}
	
	/**
	 * Removes a subdungeon from this dungeon.
	 * 
	 * @param 	subdungeon
	 * 			The dungeon to remove from this dungeon.
	 * @post	
	 */
	public void removeDungeon(Subdungeon subdungeon){
		//TODO
	}
	
	/**
	 * Checks whether this dungeon could fit in this dungeon at the given dimension.
	 * 
	 * @param 	dimension
	 * 			The dimension where the dungeon gets added.
	 * @param 	subdungeon
	 * 			The dungeon which is going to be added.
	 * @return	True if and only if there is enough space for the given dungeon to be added in this dungeon.
	 * 			| for each dimension in dungeons
	 * 			| if(dungeon.getMaxDimension()
	 */
	private boolean fit(Dimension dimension, Subdungeon subdungeon){
		for(Dimension usedDimension: subdungeon.getUsedDimensions()){
			if(!isValidDimension(usedDimension.add(dimension)))
				return false;
		}
		return true;
	}
	
	/**
	 * Return the dimension of the given Square.
	 * 
	 * @param	square
	 * 			The square of which the dimension is required.
	 * @return	The dimension which contains this square.
	 */
	protected Dimension getDimensionOf(Square square){
		for(Subdungeon dungeon : getAllDungeons()){
			Dimension dimension = dungeon.getDimensionOf(square);
			if(dimension != null)
				return dimension.add(getDimensionOfDungeon(dungeon));
		}
		return null;
	}
	
	
	public Square getSquareAt(Dimension absoluteDimension){
		Subdungeon sub = getSubDungeonAt(absoluteDimension);
		if(sub != null)
			return sub.getSquareAt(absoluteDimension.subtract(getDimensionOfDungeon(sub)));
		return null;
	}
	
	/**
	 * Returns the square at the given dimension.
	 * 
	 * @param	absoluteDimension
	 * 			The dimension to search in this dungeon.
	 * @return	The square at the given dimension.
	 * 			| dungeon.getSquareAt(relativeDimension)
	 */
	private Subdungeon getSubDungeonAt(Dimension dimension){
		for(Dimension startDimension : getDimensions()){
			if(startDimension.isSmallerThan(dimension)){
				Dimension newDimension = dimension.subtract(startDimension);
				if(newDimension.isSmallerThan(getDungeons().get(startDimension).getMaxDimension()))
					return getDungeons().get(startDimension);
			}	
		}
		return null;
	}
	
	/**
	 * Returns the starting dimension of a given subdungeon.
	 * 
	 * @param 	subdungeon
	 * 			The dungeon of which the starting dimension is required.
	 * @return	The starting dimension of this square.
	 * 			| for each dimension in getDimensions
	 * 			| if(getDungeons().get(dimension).equals(subdungeon))
	 * 			| result = dimension
	 */
	private Dimension getDimensionOfDungeon(Subdungeon subdungeon){
		for(Dimension dimension : getDimensions()){
			if(getDungeons().get(dimension).equals(subdungeon))
				return dimension;
		}
		return null;
	}
		
	/**
	 * Opens a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @effect	Open the door in the subdungeon which contains  the given square.
	 * 			| getDungeonOf(square).openDoor(square,direction)
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model
	public void openDoor(Square square, Direction direction){
		getDungeonOf(square).openDoor(square,direction);
	}
	
	/**
	 * Closes a door in the given square in the given direction.
	 * 
	 * @param 	square
	 * 			The square which contains a door.
	 * @param 	direction
	 * 			The direction in the given square where the door stands.
	 * @post	The door gets closed in the dungeon which contains the given square.
	 * 			| getDungeonOf(square).closeDoor(square,direction);
	 * @throws 	IllegalArgumentException
	 * 			The given square doesn't exist in this room.
	 * 			| getDimensionOf(square) == null
	 * @throws 	IllegalArgumentException
	 * 			There is no door in the given direction or the square isn't part of this dungeon.
	 * 			| !isDoor(square, direction)
	 */
	@Model
	public void closeDoor(Square square, Direction direction){
		getDungeonOf(square).closeDoor(square,direction);
	}
	
	/**
	 * Removes a square at the given dimension.
	 * 
	 * @param	dimension
	 * 			The dimension on which the square must be removed.
	 * @effect	Remove in the dungeon that contains the given dimension, the square at that dimension.
	 * 			| getDungeonAt(dimension).removeSquareAt(dimension.subtract(getDimensionOfDungeon(getDungeonAt(dimension))))
	 */
	public void removeSquareAt(Dimension dimension){
		if(getSubDungeonAt(dimension)!=null)
			getSubDungeonAt(dimension).removeSquareAt(dimension.subtract(getDimensionOfDungeon(getSubDungeonAt(dimension))));
	}
	
	/**
	 * Check whether the given square has already been used.
	 * 
	 * @param	square
	 * 			The square the used state must be returned.
	 * @return	True if and only if this square has already been used.
	 * 			| for each dungeon in getAllDungeons()
	 * 			| result = dungeon.hasBeenUsed()
	 */
	public boolean hasBeenUsed(Square square){
		for(Subdungeon dungeon: getAllDungeons()){
			if(dungeon.hasBeenUsed(square))
				return true;
		}
		return false;
	}
	
	/**
	 * Returns the relative dimension of the given dimension in the given dungeon.
	 * 
	 * @param 	dungeon
	 * 			The subdungeon in which the dimension is wanted.
	 * @param 	dimension
	 * 			The absolute dimension in this dungeon.
	 * @return	The relative dimension in the given dungeon, that equals the given dimension in this dungeon.
	 */
	private Dimension getDimensionIn(Subdungeon dungeon, Dimension dimension){
		return dimension.subtract(dungeon.getMaxDimension());
	}
	
	/**
	 * Returns the dungeon that contains the given square.
	 * 
	 * @param 	square
	 * 			The square in this dungeon of which the subdungeon is wanted.
	 * @return	The dungeon that contains the dimension on which the square lies.
	 * 			| getDungeonAt(getDimensionOf(square))
	 */
	private Subdungeon getDungeonOf(Square square){
		return getSubDungeonAt(getDimensionOf(square));
	}
	
	/**
	 * Adds a square to this dungeon at the given position.
	 * 
	 * @param	square
	 * 			The square to add.
	 * @param	dimension
	 * 			The dimension where to add.
	 * @effect	The square gets added to the dungeon which contains the given dimension.
	 * 			| getDungeonAt(dimension).addSquare(square, getDimensionIn(getDungeonAt(dimension), dimension))
	 */
	public void addSquare(Square square, Dimension dimension){
		super.addSquare(square, dimension);
		getSubDungeonAt(dimension).addSquare(square, getDimensionIn(getSubDungeonAt(dimension), dimension));
	}
	
	/**
	 * Return the given dimension in absolute value.
	 * 
	 * @param	dimension
	 * 			The given dimension
	 * @return	The dimension in the given root dungeon.
	 * 			| dimension.add(getDimensionOf(dungeon))
	 * 			| if(getContainingDungeon()!= null)
	 * 			| result = getContainingDungeon().getRootDimension(dimension)
	 * 			| result = dimension
	 */
	public Dimension getRootDimension(Dimension dimension,Subdungeon dungeon){
		if(!(getDimensionOfDungeon(dungeon)==null || dungeon.equals(this)))
				dimension = dimension.add(getDimensionOfDungeon(dungeon));
		if(getContainingDungeon()!=null)
			return getContainingDungeon().getRootDimension(dimension, dungeon);
		return dimension;
	}
	
	/**
	 * Returns a collection of all the dimension used in this dungeon.
	 */
	public Collection<Dimension> getUsedDimensions(){
		Set<Dimension> dimensions = new HashSet<Dimension>();
		for(Subdungeon dungeon: getAllDungeons()){
			for(Dimension dimension : dungeon.getUsedDimensions()){
				dimensions.add(dimension.add(getDimensionOfDungeon(dungeon)));
			}
		}
		return dimensions;
	}
	
	/**
	 * Returns the room that contains this square.
	 */
	public Room getRoomOf(Square square){
		return getDungeonOf(square).getRoomOf(square);
	}
}
